﻿#region Copyright & Header Information
/*
 * This code is provided AS-IS by me, Rory Blyth, of Neopoleon.com. I not responsible for anything
 * bad that might result from this code. Nobody at Neopoleon.com is responsible.
 *  
 * If this code goes nuts and tries to get your laptop to mate with your internet-aware
 * refrigerator, then I am not responsible.
 * 
 * If this code somehow causes World War III (or any other war), then I am not responsible.
 * 
 * Basically, I take no responsibility for anything.
 * 
 * However, if you like it, then you can feel free to drop me a nice little thank you note
 * at "rory@neopoleon.com" - If you don't like it, then you can also write to tell me why.
 * 
 * I don't mind.
 * 
 * Also, don't remove this header. It just wouldn't be berry nice if you did.
 */
/*****************************************************************************/
/* CHANGE HISTORY:
 *		March 6, 2004
 *			- Created for PADNUG by Rory Blyth.
 *			- Added @variable/@define support with file dependency caching.
 *			- Post: http://www.neopoleon.com/home/blogs/neo/archive/2004/03/06/8705.aspx
 *		October 11, 2007
 *			- Updated by Gabe Moothart.
 *			- Added relative url qualification.
 *			- Project: http://www.codeplex.com/CssHandler/
 *		November 14, 2007
 *			- Updated by Troy Goode.
 *			- Added server-side expansion of @import directives.
 *			- Added whitespace removal.
 *			- Added removal of @define block before sending file to client.
 *			- Added ability to define handler as a .axd and get file from querystring.
 *			- Fixed bug where similiarly named variables could cause improper parsing.
 *			- Re-organized comments and documentation.
 *			- Post: ?
 *		June 20, 2009
 *			- Added change made by Lee (easytree@gmail.com) to make sure the last variable declaration "wins" when multiple declarations are made with the same key.
 */
/*****************************************************************************/
/* AUTHORS:
 *		Rory Blyth
 *			- Blog: http://www.neopoleon.com/
 *			- Email: rory@neopoleon.com
 *		Gabe Moothart
 *			- Blog: http://codingpatterns.blogspot.com/
 *			- Email: gmoothart@gmail.com
 *		Troy Goode
 *			- Blog: http://www.squaredroot.com/
 *			- Email: tgoode@squaredroot.com
 */
/*****************************************************************************/
/* INSTRUCTIONS:
 * 
 * Method 1: Handling all .css files.
 * ----------------------------------
 * 
 * Add the following to the <httpHandlers> element inside the <system.web> element of your web.config:
 *    <add verb="GET" path="*.css" type="CssHandler.CssHandler, CssHandler"/>
 * 
 * All .css files in your application will now be parsed by the CssHandler.
 * 
 * NOTE: This method will mean that your css files also are covered by ASP.Net security. In most cases
 * this will mean you will want to add the following web.config file to the directory where your css files
 * are kept:
 * 
 *	<?xml version="1.0"?>
 *	<configuration>
 *		<system.web>
 *			<authorization>
 *				<allow users="*" />
 *			</authorization>
 *		</system.web>
 *	</configuration>
 * 
 * Method 2: Handling individual .css files explicitly.
 * ----------------------------------------------------
 * 
 * Add the following to the <httpHandlers> element inside the <system.web> element of your web.config:
 *    <add verb="GET" path="CssHandler.axd" type="CssHandler.CssHandler, CssHandler" validate="false" />
 * 
 * To have a .css file parsed by the CssHandler, instead of including it like...
 *    <link href="styles.css" rel="StyleSheet" type="text/css" />
 * ... include it like...
 *    <link href="CssHandler.axd?styles.css" rel="StyleSheet" type="text/css" />
 * 
 * NOTE: Only files ending with ".css" will be parsed. All other files will return a 403 Forbidden
 * (even if the requested file does not actually exist).
 * 
 */
#endregion

#region Using Statements
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Caching;
#endregion

namespace CssHandler{
	public class CssHandler : IHttpHandler{

		#region Constants & Fields

		private const string REGEX_DEFINITION_BLOCK = @"@define\s*{(?<defines>[^}]*)}";
		private const string REGEX_IMPORT_STATEMENT = @"@import\s*url\((?<importUrl>[^\)]*)\);";
		private const string REGEX_COMMENTS = @"/\*.+?\*/";
		private const string REGEX_RELATIVE_URLS = @"url\s*\(\s*(?<url>~.*)\s*\)";
		private const string CACHE_KEY_PREFIX = "CssHandler-File: ";
		private const int MAXIMUM_IMPORTS = 99;

		private List<string> fileNames = new List<string>();

		#endregion

		#region IHttpHandler Implementation

		/// <summary>
		/// IHttpHandler method - We return false because this handler is not thread-safe and a new instance should be create for each request. [Created by Rory Blyth, 3-6-2004.]
		/// </summary>
		public bool IsReusable{ get{ return false; } }

		/// <summary>
		/// IHttpHandler method - This is the entry point for our handler. Here we decide if we'll process a file based upon the requested path or a path passed via the querystring. [Created by Rory Blyth, 3-6-2004.]
		/// </summary>
		/// <param name="context">The HttpContext associated with this request.</param>
		public void ProcessRequest( System.Web.HttpContext context ){

			// Load file based upon what file was requested
			string topFile = context.Request.PhysicalPath.Trim();

			// Override if another file was specified in the querystring
			if( context.Request.QueryString != null && context.Request.QueryString.ToString().Length > 0 )
			{
				topFile = context.Request.QueryString.ToString().Trim();
				topFile = context.Server.MapPath(topFile);
			}

			// Process css file. Handle 404 and 500 errors appropriately.
			try{

                // Parse the css file
                string css = ProcessCss( context, topFile );

                // to aid in debugging by outputting the css directly to the
                // client rather than forcing a download set the contenttype back to "text/html"
                //context.Response.ContentType = "text/html";
                context.Response.ContentType = "text/css";

				// Output the css to the client
				context.Response.Write( css );

			}catch( FileNotFoundException fileNotFound ){

				// Get filename but make it safe for revealing to public (ie: no path)
				string fileName = fileNotFound.Message;
				fileName = fileName.Replace( "\\", "/" );
				fileName = fileName.Substring( fileName.LastIndexOf("/") + 1 );

				// Output error to client
				context.Response.StatusCode = 404;
				context.Response.Write( "File Not Found: " + fileName );

			}catch( AccessViolationException notAllowed ){

				// Get filename but make it safe for revealing to public (ie: no path)
				string fileName = notAllowed.Message;
				fileName = fileName.Replace( "\\", "/" );
				fileName = fileName.Substring( fileName.LastIndexOf("/") + 1 );

				// Output error to client
				context.Response.StatusCode = 403;
				context.Response.Write( "Access Denied: " + fileName );

			}catch( Exception ex ){
				// Output error to client
				context.Response.StatusCode = 500;
				context.Response.Write( "Error: " + ex.Message );
			}

		}

		#endregion

		#region Css Processing

		/// <summary>
		/// Processes specified css files, performing several operations on it until it can be
		/// delivered to the client and parsed as a normal css file. [Created by Rory Blyth, 3-6-2004.]
		/// </summary>
		/// <param name="context">The HttpContext associated with this request.</param>
		/// <param name="topFile">File path for first file to be loaded.</param>
		private string ProcessCss( HttpContext context, string topFile ){

			// Only load files ending in .css
			if( !topFile.EndsWith(".css") )
				throw new AccessViolationException(topFile);

			// Verify that the file exists
			if (!File.Exists(topFile))
				throw new FileNotFoundException(topFile);

			// Try to load css from cache
			string css = context.Cache[ CACHE_KEY_PREFIX + topFile ] as string ?? "";

			// Process css and cache it if it is not already cached or IS_DEBUG
			if( css == "" ){

				// Read the contents of the file into a variable named "body."
				StreamReader reader = new StreamReader(topFile);
				css = reader.ReadToEnd();
				reader.Close();

				// Add the filename to the list of dependencies
				fileNames.Add(topFile);

				// Expand @import directives.
				css = ExpandImportDirectives( context, css, 1 );

				// Now that we've stripped the comments, we can go ahead and process the CSS.
				css = ProcessStyleSheet(css);

				// The @define block should not be sent to the client, so remove it
				css = Regex.Replace( css, REGEX_DEFINITION_BLOCK, "", RegexOptions.Singleline );

				// Collapse all the whitespace to save some bandwidth
				css = Regex.Replace( css, @"\s+", " ", RegexOptions.Singleline );
				css = Regex.Replace( css, @"\s{", "{", RegexOptions.Singleline );
				css = Regex.Replace( css, @"\s*:\s*", ":", RegexOptions.Singleline );
				css = css.Trim();

				// Cache for later use
				CacheDependency fileDependency = new CacheDependency( fileNames.ToArray() );
				context.Cache.Insert( CACHE_KEY_PREFIX + topFile, css, fileDependency );

			}

			// Output the parsed css to the client
            return css;

		}

		/// <summary>
		/// Recursively loads css files specified by @import statements until all
		/// @imports have been processed or the import limit has been reached.
		/// [Added by Troy Goode, 11-14-2007.]
		/// </summary>
		/// <param name="context">The HttpContext associated with this request.</param>
		/// <param name="css">The css text.</param>
		/// <param name="count">The number of imports that have occurred.</param>
		/// <returns>The css text that was passed in with @import statements replaced with the
		/// css text of the file that was referenced.</returns>
		private string ExpandImportDirectives( HttpContext context, string css, int count )
		{

			// Prevent a runaway process by capping the number of imports. Otherwise circular
			// references could create an infinite loop.
			if( count > MAXIMUM_IMPORTS )
				throw new Exception( "Maximum imports exceeded." );

			// Strip the comments from the CSS document. We don't want to accidently
			// expand an @import statement that has been commented out and generate a 404.
			css = Regex.Replace( css, REGEX_COMMENTS, "", RegexOptions.Singleline );

            // Now, fully qualify any relative urls so we can find the css files in the
            // appropriate locations
            css = QualifyRelativeUrls(css);

			// Loop through all @import directives and expand them into the css
			Regex regex = new Regex( REGEX_IMPORT_STATEMENT, RegexOptions.Singleline );
			Match match = regex.Match(css);
			StringBuilder sb = new StringBuilder(css);
			while( match.Success ){

				// Retrieve url
				string url = match.Groups["importUrl"].Value;

				// Only load files ending in .css
				if( !url.EndsWith(".css") )
					throw new AccessViolationException(url);

				// Find url's physical location
				string fileToImport = context.Server.MapPath(url);

				// verify file exists
				if( !File.Exists(fileToImport) )
					throw new FileNotFoundException(fileToImport);

				// Read specified file and retrieve its css contents
				StreamReader stream = File.OpenText(fileToImport);
				string text = stream.ReadToEnd();
				stream.Close();

				// Add file to list of file dependencies
				fileNames.Add(fileToImport);

				// Process new css contents and then add to the full css to be returned
				text = ExpandImportDirectives( context, text, count++ );
				sb.Append( text );
				match = match.NextMatch();

			}

			// Remove the @import directives before sending to client so that client does
			// not download extraneous css files
			css = sb.ToString();
			css = Regex.Replace( css, REGEX_IMPORT_STATEMENT, "", RegexOptions.Singleline );
			return css;

		}

		/// <summary>
		/// Scans the input for asp.net app-relative '~' markers, and replaces
		/// them with the fully-qualified path. [Added by Gabe Moothart, 10-11-2007.]
		/// </summary>
		/// <param name="body">The css text.</param>
		/// <returns>The css with relative paths replaced by absolute paths.</returns>
		private string QualifyRelativeUrls( string css )
		{

			// Find and loop through all the css urls with the asp.net '~' character
			Regex regex = new Regex( REGEX_RELATIVE_URLS );
			Match match = regex.Match(css);
			while( match.Success ){

				// replace each url in the css with the qualified url
				string url = match.Groups["url"].Value;
				css = css.Replace( url, VirtualPathUtility.ToAbsolute(url) );
				match = match.NextMatch();

			}
			return css;

		}

		/// <summary>
		/// Loads a list of key-value pairs from an @define{...} block, then replaces the keys found
		/// found throughout the rest of the css with the specified value. [Created by Rory Blyth, 3-6-2004.]
		/// </summary>
		/// <param name="css">The css text.</param>
		/// <returns>The css text with variables replaced by the css constant values.</returns>
        private string ProcessStyleSheet( string css ){

			// Find the @define{...} block
			Regex regex = new Regex( REGEX_DEFINITION_BLOCK, RegexOptions.Singleline );
			Match match = regex.Match(css);

			// If no @define{...} block was found exit out
			if( !match.Success )
				return css;

			// The regular expression ought to have isolated the variable name/value pairs
			// in the @define block. We're going to separate these key/value pairs out into
			// a string array so that we can manipulate them individually.
			string[] lines = match.Groups["defines"].Value.Split( new char[]{';'} );

            // Turn the list into key value pairs so that we can access the key atomically.
            // Store the definitions in a dictionary, keyed by canonicalized variableName so that if there
            // are multiple definitions for a particular variable, the last one discovered 'wins out' (as in raw css.)
            Dictionary<string, string> variableLookup = new Dictionary<string, string>();
			foreach( string line in lines ){

				// Did we get a blank line? If so, then let's move onto the next.
				if( line.Trim() == "" )
					continue;

				// The CSS assignment operator is the colon (':') - We'll split the line on
				// this character, resulting in an array with two elements. The first
				// element will be the variable name, while the second will be the variable
				// value.
				string[] tokens = line.Split( new char[]{ ':' } );
				string variableName = tokens[0].Trim();
				string variableValue = tokens[1].Trim();

				// Add to lookup
                variableLookup[variableName] = variableValue;
			}

            // Convert the lookup into a list, ready for sorting
            List<KeyValuePair<string, string>> definitions = new List<KeyValuePair<string, string>>();

            foreach (string variableName in variableLookup.Keys)
            {
                KeyValuePair<string, string> pair = new KeyValuePair<string, string>(variableName, variableLookup [variableName]);
                definitions.Add(pair);
            }

            // Sort the array by each key's length (descending) to avoid false positives. Otherwise if both
			// "@bgColor" and "@bgColor2" are defined @bgColor2 could wind up having itself
			// partially overwritten. 
			definitions.Sort(
				delegate( KeyValuePair<string,string> left, KeyValuePair<string,string> right ){
					return right.Key.Length.CompareTo( left.Key.Length );
				}
			);

			// Iterate through each element of the "lines" array. Each string in the "lines"
			// array should consist of one key/value pair. we can now
			// replace all of the instances of the variable name in the rest of the
			// CSS document with the variable's value. This is where all the "magic"
			// happens.
			StringBuilder sb = new StringBuilder(css);
			definitions.ForEach( delegate( KeyValuePair<string,string> pair ){
				sb.Replace( "@" + pair.Key, pair.Value );
			});
			return sb.ToString();

		}

		#endregion

	}
}